package de.fhkl.thatsstroke;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
import android.os.StrictMode;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.util.Log;
import de.fhkl.strokeprotocol.io.StrokeInputReader;
import de.fhkl.strokeprotocol.io.StrokeOutputWriter;
import de.fhkl.strokeprotocol.listeners.ICloseListener;
import de.fhkl.strokeprotocol.listeners.IMessageListener;
import de.fhkl.strokeprotocol.messages.AcceptRatingMessage;
import de.fhkl.strokeprotocol.messages.AcceptVideoMessage;
import de.fhkl.strokeprotocol.messages.IMessage;
import de.fhkl.strokeprotocol.messages.LoginAnswerMessage;
import de.fhkl.strokeprotocol.messages.LoginMessage;
import de.fhkl.strokeprotocol.messages.LogoutMessage;
import de.fhkl.strokeprotocol.messages.NewVideoMessage;
import de.fhkl.strokeprotocol.messages.PingMessage;
import de.fhkl.strokeprotocol.messages.RatingMessage;
import de.fhkl.strokeprotocol.messages.RecordingStartsMessage;
import de.fhkl.strokeprotocol.messages.RecordingStopsMessage;
import de.fhkl.strokeprotocol.messages.TransmissionCompletedMessage;
import de.fhkl.strokeprotocol.messages.VideoAssignedMessage;
import de.fhkl.strokeprotocol.messages.VideoMessage;
import de.fhkl.strokeprotocol.messages.VideoStartsMessage;
import de.fhkl.thatsstroke.database.Video;
import de.fhkl.thatsstroke.database.VideoDataSource;

/**
 * Underlying service of the WhatsStroke for network communication with the
 * StrokeServer.
 */
public class StrokeService extends Service implements ICloseListener
{
	/** Possible system states */
	private static enum SystemStatus
	{
		CONTACT_AVAILABLE, SERVER_AVAILABLE, OFFLINE
	}

	/** Unique identifier for status notifications */
	private static final int STATUS_NOTIFICATION = 2;

	/** Time interval between pings */
	private static final int PING_INTERVAL = 30000;

	/** Time to wait before the service will try to reconnect */
	private static final int TIMEOUT_INTERVAL = 10000;

	/** Constants for shared preference identifiers */
	private static final String SERVER_ADDRESS = "serverAddress";
	private static final String SERVER_PORT = "serverPort";

	/**
	 * Vibrator pattern starting with a pause followed by a vibrate in
	 * milliseconds.
	 */
	private static final long[] RRRRING = { 80, 100, 200, 100, 200, 100, 200, 100, 50 };
	private static final long[] RRING = { 80, 100, 200, 100, 50 };

	/**
	 * Given user email, this is necessary to reconnect after a connection loss
	 */
	private String mEmail;
	/**
	 * Given user password, this is necessary to reconnect after a connection
	 * loss
	 */
	private String mPassword;

	/** Server address */
	private InetAddress mInetAddress;

	/** Server port */
	private int mPort;

	/** Socket for the connection to the server */
	private Socket mSocket;

	/** Output stream to send messages to the server */
	private StrokeOutputWriter mOut;

	/** Input stream to listen for messages from the server */
	private StrokeInputReader mInputListener;

	/** Is there an open connection to the server? */
	private boolean mIsConnected;

	/** Is the user already successfully logged in to the server? */
	private boolean mIsLoggedIn;

	/**
	 * Database connection to load and store the rating states and progresses of
	 * videos.
	 */
	private VideoDataSource mDataSource;

	/** We use shared preferences for configs */
	private SharedPreferences mPreferences;

	/**
	 * Contains mapping from a video to the according FileOutputStream, so
	 * multiple Videos can be written at the same time.
	 */
	private Map<UUID, FileOutputStream> mStreamMap;

	/** Ping timer */
	private PingTimer mPingTimer;

	/** Timeout timer */
	private TimeoutTimer mTimeoutTimer;

	/** Notification manager allows us to set notifications in the icon tray */
	private NotificationManager mNotManager;

	/** Current status notification */
	private Notification mNotification;

	/** Previous system status */
	private SystemStatus mPreviousStatus;

	@TargetApi(Build.VERSION_CODES.GINGERBREAD)
	@Override
	public void onCreate()
	{
		Log.i("Service", "onCreate " + this);

		super.onCreate();

		// Suppresses network on main thread exceptions implemented in version
		// 3.0. Testing the app mostly on 2.2 to 2.3 devices did not show any
		// strange errors occurring from the blocking network connection. You
		// should reconsider this when continuing development. Should be fine
		// for evaluation prototype.
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD)
		{
			StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
			StrictMode.setThreadPolicy(policy);
		}

		mDataSource = new VideoDataSource(this);
		mDataSource.open();

		mPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());

		mStreamMap = new HashMap<UUID, FileOutputStream>();

		mPingTimer = new PingTimer();
		mTimeoutTimer = new TimeoutTimer();

		registerReceiver(mPingTimer, new IntentFilter(Broadcast.PING_TIMER));
		registerReceiver(mTimeoutTimer, new IntentFilter(Broadcast.TIMEOUT_TIMER));
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId)
	{
		Log.i("Service", "onStartCommand " + this);

		super.onStartCommand(intent, flags, startId);

		return START_STICKY;

	}

	@Override
	public void onDestroy()
	{
		this.unregisterReceiver(mPingTimer);
		this.unregisterReceiver(mTimeoutTimer);

		mDataSource.close();

		super.onDestroy();
	}

	@Override
	public IBinder onBind(Intent intent)
	{
		mNotManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

		Log.i("Service", "onBind");

		return new StrokeBinder(this);
	}

	@Override
	public void onClose(Object obj)
	{
		setNotification(SystemStatus.OFFLINE);

		Log.i("Service", "onClose");

		mTimeoutTimer.startBackOff();
	}

	@SuppressWarnings("deprecation")
	private void setNotification(SystemStatus status)
	{
		if (mPreviousStatus != status)
		{
			mPreviousStatus = status;

			// Remove old notification before setting a new one
			mNotManager.cancel(STATUS_NOTIFICATION);

			// Create new notification by given status
			mNotification = new Notification();
			mNotification.setLatestEventInfo(this, getText(R.string.app_name), getText(R.string.notification_description),
					PendingIntent.getActivity(this, 1, new Intent(this, Login.class), 0));

			switch (status)
			{
				case CONTACT_AVAILABLE:
					mNotification.icon = R.drawable.status_contact_available;

				break;
				case SERVER_AVAILABLE:
					mNotification.icon = R.drawable.status_server_available;

				break;
				case OFFLINE:
					mNotification.icon = R.drawable.status_offline;
			}
		}

		// Set the notification
		mNotManager.notify(STATUS_NOTIFICATION, mNotification);
	}

	/**
	 * Try a login on the specified server with the given email and password.
	 * 
	 * @param email
	 * @param password
	 * 
	 * @return boolean Indicates that the connection to the server was
	 *         successfully established, NOT a successful login.
	 */
	public boolean login(String email, String password)
	{
		try
		{
			if (!mIsConnected)
			{
				if (!connect())
				{
					return false;
				}
			}

			mEmail = email;
			mPassword = password;

			Log.i("Service", "Login");

			mOut.send(new LoginMessage(email, password, 'd'));
		}
		catch (Exception e)
		{
			Log.e("Service", "Exception " + e.getMessage());

			return false;
		}

		return true;
	}

	/**
	 * Try to log out from the server and stop this service.
	 */
	public void logout()
	{
		try
		{
			mOut.send(new LogoutMessage());

			disconnect();
		}
		catch (Exception e)
		{
			Log.e("Service", "Exception " + e.getMessage());
		}

		mIsLoggedIn = false;
		mEmail = null;
		mPassword = null;

		mNotManager.cancel(STATUS_NOTIFICATION);
	}

	/**
	 * Tells the server that a video playback with given id has started
	 * 
	 * @param videoId
	 * @return
	 */
	public boolean videoStarts(UUID videoId)
	{
		Log.i("Service", "Video starts " + videoId);

		mDataSource.open();
		mDataSource.updateVideoFlag(videoId, Inbox.VIEWED);

		return sendMessage(new VideoStartsMessage(videoId));
	}

	/**
	 * Tells the server whether or not a video has been accepted to get viewed
	 * and rated.
	 * 
	 * @param videoId
	 * @param accepted
	 * @return
	 */
	public boolean acceptVideo(UUID videoId, boolean accepted)
	{
		return acceptVideo(videoId, accepted, 0);
	}

	/**
	 * Tells the server whether or not a video has been accepted to get viewed
	 * and rated.
	 * 
	 * @param videoId
	 * @param accepted
	 * @param startWithPart
	 * @return
	 */
	public boolean acceptVideo(UUID videoId, boolean accepted, int startWithPart)
	{
		Log.i("Service", "Video accepted " + videoId + " startsWithPart: " + startWithPart);

		if (accepted)
		{
			prepareVideoFile(videoId);
		}

		return sendMessage(new AcceptVideoMessage(videoId, accepted, startWithPart));
	}

	/**
	 * Sends the rating of the video with the given id to the server
	 * 
	 * @param receiverId
	 * @param videoId
	 * @param isStroke
	 * @return
	 */
	public boolean sendRating(UUID videoId, boolean isStroke)
	{
		Log.i("Service", "Send rating " + videoId);

		return sendMessage(new RatingMessage(videoId, isStroke));
	}

	/**
	 * Tries to send the given message. Tries 5 times to reconnect if the
	 * transmission failed. Starts a timer that periodically tries to reconnect
	 * if this method failed to reconnect.
	 * 
	 * @param message
	 * @return true if the message was successfully sent
	 */
	private boolean sendMessage(IMessage message)
	{
		return sendMessage(message, 5);
	}

	/**
	 * Tries to send the given message. Tries to reconnect if the transmission
	 * failed. Starts a timer that periodically tries to reconnect if this
	 * method failed to reconnect.
	 * 
	 * @param message
	 * @param retries
	 *            number of retries before we give up reconnecting, won't stop
	 *            trying to reconnect if retries = 0
	 * @return true if the message was successfully sent
	 */
	private boolean sendMessage(IMessage message, int retries)
	{
		boolean successfulReconnect = false;

		for (int i = 0; i < retries || successfulReconnect; i++)
		{
			try
			{
				mOut.send(message);
				successfulReconnect = true;
				break;
			}
			catch (Exception e)
			{
				Log.e("Service", "Exception " + e.getMessage());

				try
				{
					Thread.sleep(TimeoutTimer.BACKOFF_MIN);
				}
				catch (InterruptedException ie)
				{
					Log.e("Service", "InterruptedException " + ie.getMessage());
				}

				successfulReconnect = reconnect();
			}
		}

		if (!successfulReconnect)
		{
			mTimeoutTimer.startBackOff();
		}

		return successfulReconnect;
	}

	/**
	 * Sets up the connection to the server
	 * 
	 * @throws IOException
	 * @throws SocketTimeoutException
	 */
	private boolean connect() throws IOException, SocketTimeoutException
	{
		try
		{
			mInetAddress = InetAddress.getByName(mPreferences.getString(SERVER_ADDRESS, ""));
		}
		catch (UnknownHostException e)
		{
			Log.e("Service", "UnknownHostException " + e.getMessage());
			return false;
		}

		mPort = mPreferences.getInt(SERVER_PORT, 50001);

		// open tcp connection
		SocketAddress addr = new InetSocketAddress(mInetAddress, mPort);
		mSocket = new Socket();
		mSocket.connect(addr, TIMEOUT_INTERVAL / 2);

		Log.i("Service", "Socket erzeugt " + mSocket);

		// open output and input streams
		mOut = new StrokeOutputWriter(mSocket.getOutputStream());
		mInputListener = new StrokeInputReader(mSocket.getInputStream());

		// register listeners
		mInputListener.setMessageListener(LoginAnswerMessage.class, new LoginAnswerListener());
		mInputListener.setMessageListener(PingMessage.class, new PingListener());
		mInputListener.setMessageListener(RecordingStartsMessage.class, new RecordingStartsListener());
		mInputListener.setMessageListener(RecordingStopsMessage.class, new RecordingStopsListener());
		mInputListener.setMessageListener(NewVideoMessage.class, new NewVideoListener());
		mInputListener.setMessageListener(VideoMessage.class, new VideoListener());
		mInputListener.setMessageListener(TransmissionCompletedMessage.class, new TransmissionCompletedListener());
		mInputListener.setMessageListener(AcceptRatingMessage.class, new AcceptRatingListener());
		mInputListener.setMessageListener(VideoAssignedMessage.class, new VideoAssignedListener());

		// start input reader
		mInputListener.start(this);

		mIsConnected = true;

		Log.i("Service", "Connected to " + mInetAddress + ":" + mSocket);

		return true;
	}

	/**
	 * Closes the connection to the server
	 */
	private void disconnect()
	{
		mIsConnected = false;
		mIsLoggedIn = false;

		mPingTimer.cancel();
		mTimeoutTimer.cancel();

		// make sure inputListener is closed before closing outputWriter
		if (mInputListener != null)
		{
			mInputListener.stop();
			mInputListener = null;
		}

		try
		{
			if (mOut != null)
			{
				mOut.close();
				mOut = null;
			}
		}
		catch (Exception e)
		{
			Log.e("Service", "Exception " + e.getMessage());
		}

		try
		{
			if (mSocket != null)
			{
				mSocket.close();
				mSocket = null;
			}
		}
		catch (Exception e)
		{
			Log.e("Service", "Exception " + e.getMessage());
		}

		Log.i("Service", "Disconnected");
	}

	/**
	 * @return boolean Indicates that the connection to the server was
	 *         successfully established, NOT a successful reconnect.
	 */
	private boolean reconnect()
	{
		disconnect();
		if (login(mEmail, mPassword))
		{
			mTimeoutTimer.cancel();

			return true;
		}

		return false;
	}

	/**
	 * Is the service connected to the server and logged in?
	 * 
	 * @return
	 */
	public boolean isLoggedIn()
	{
		return mIsLoggedIn;
	}

	/**
	 * Converts a videoId into its storage path on the SD card.
	 * 
	 * @param videoId
	 * @return Path to the file with the given videoId or null if an error
	 *         occurred
	 */
	private File getMediaFile(UUID videoId)
	{
		File mediaFile = null;

		// To be safe, you should check that the SDCard is mounted
		if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
		{
			Log.i("StrokeService", "SDCard media mounted");

			File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
					"ThatsStroke");

			// Create the storage directory if it does not exist
			if (!mediaStorageDir.exists())
			{
				if (!mediaStorageDir.mkdirs())
				{
					Log.e("Inbox", "failed to create directory");
					return null;
				}
			}

			mediaFile = new File(mediaStorageDir.getPath() + File.separator + videoId + ".mp4");
		}

		return mediaFile;
	}

	/**
	 * Setting up the OutputStream for storing a video file on the SDCard
	 * 
	 * @param videoId
	 * @return
	 */
	private boolean prepareVideoFile(UUID videoId)
	{
		File mediaFile = getMediaFile(videoId);

		FileOutputStream outputStream = null;

		try
		{
			if (mStreamMap.containsKey(videoId))
			{
				outputStream = mStreamMap.get(videoId);
			}
			else
			{
				outputStream = new FileOutputStream(mediaFile, true);
			}
		}
		catch (FileNotFoundException e)
		{
			Log.e("Inbox", "FileNotFoundException " + e.getMessage());
			return false;
		}

		mStreamMap.put(videoId, outputStream);

		mDataSource.updateVideoFlag(videoId, Inbox.LOADING);

		return true;
	}

	/**
	 * Writes byte data from the server into the corresponding video file.
	 * Updates a ProgressBar.
	 * 
	 * @param videoId
	 * @param videoData
	 */
	private void writeVideoFile(final UUID videoId, byte[] videoData)
	{
		FileOutputStream outputStream = mStreamMap.get(videoId);

		try
		{
			outputStream.write(videoData);
			outputStream.flush();

			videoData = null;

			mDataSource.updateVideoProgress(videoId);
		}
		catch (IOException e)
		{
			Log.e("Inbox", "IOException " + e.getMessage());
		}
	}

	/**
	 * Closes the OutputStream when the transmission of a file is complete.
	 * Minor changes to the GUI so the video can be viewed.
	 * 
	 * @param videoId
	 */
	private void finalizeVideoFile(final UUID videoId)
	{
		try
		{
			mStreamMap.get(videoId).close();
			mStreamMap.remove(videoId);

			mDataSource.updateVideoFlag(videoId, Inbox.LOADED);
		}
		catch (IOException e)
		{
			Log.e("Inbox", "IOException " + e.getMessage());
		}
	}

	/**
	 * Deletes the video file associated with the given videoId from the SD card
	 * 
	 * @param videoId
	 */
	private void deleteVideoFile(UUID videoId)
	{
		File mediaFile = getMediaFile(videoId);

		if (mediaFile != null && mediaFile.exists())
		{
			mediaFile.delete();
		}

		Log.i("StrokeService", "Video file deleted " + videoId);
	}

	/**
	 * Deletes all old video files from the database and SD card.
	 * 
	 * @param list
	 */
	public void cleanUpVideos()
	{
		List<UUID> list = mDataSource.cleanupOldVideos();

		for (UUID id : list)
		{
			deleteVideoFile(id);
		}
	}

	/**
	 * Plays a ringtone and lets the phone vibrate with the given pattern.
	 * 
	 * @param pattern
	 */
	private void alert(long[] pattern)
	{
		Uri alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
		if (alert == null)
		{
			// alert is null, using backup
			alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
			if (alert == null)
			{
				// alert is still null, using second backup
				alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
			}
		}

		Ringtone r = RingtoneManager.getRingtone(getApplicationContext(), alert);
		Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);

		r.play();

		if (vibrator != null)
		{
			vibrator.vibrate(pattern, -1);
		}
	}

	/**
	 * Listener for LoginAnswerMessages.<br>
	 * Cancels the timeout timer, then sends a broadcast intent to inform the
	 * activity and starts the ping timer if the login was successful.
	 */
	private class LoginAnswerListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
			// cancel timeout timer
			mTimeoutTimer.cancel();

			LoginAnswerMessage msg = (LoginAnswerMessage) message;
			boolean isSuccessful = msg.isSuccessful();

			// start ping timer if login was successful
			if (isSuccessful)
			{
				try
				{
					mOut.send(new PingMessage());
					mIsLoggedIn = true;
				}
				catch (Exception e)
				{
					Log.e("Service", "Exception " + e.getMessage());
				}

				mDataSource.deleteNonacceptedVideos();

				mPingTimer.start(PING_INTERVAL);

				List<Video> list = mDataSource.getAllUnfinishedVideos();

				for (Video v : list)
				{
					acceptVideo(UUID.fromString(v.getVideoId()), true, (int) v.getProgress());
				}

				Intent intent = new Intent(Broadcast.UPDATE_VIEW);
				sendBroadcast(intent);
			}
			else
			{
				// close reader, writer, socket etc
				logout();
			}

			// inform activity
			Intent intent = new Intent(Broadcast.LOGIN_ANSWER);
			intent.putExtra("de.fhkl.thatsstroke.LoginAnswer", isSuccessful);
			sendBroadcast(intent);

			Log.i("Service", "onLoginAnswer " + isSuccessful);
		}
	}

	/**
	 * Listener for PingMessages.<br>
	 * Cancels the time out timer and updates the status notification.
	 */
	private class PingListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
			// cancel timeout
			mTimeoutTimer.cancel();

			if (((PingMessage) message).isReceiverAvailable())
			{
				setNotification(SystemStatus.CONTACT_AVAILABLE);

				Log.i("Service", "Ping: Contact available");
			}
			else
			{
				setNotification(SystemStatus.SERVER_AVAILABLE);

				Log.i("Service", "Ping: Server available");
			}
		}
	}

	/**
	 * Listener for RecordingStartsMessage.<br>
	 * Activates the ringtone and vibrator to get the doctors attention for a
	 * new video that is coming soon. Sends a broadcast to the activity so it
	 * can update the view.
	 */
	private class RecordingStartsListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
			alert(RRING);

			Intent intent = new Intent(Broadcast.RECORDING_STARTS);
			sendBroadcast(intent);

			Log.i("Service", "Recording Starts");
		}
	}

	/**
	 * Listener for RecordingStopsMessages.<br>
	 * Indicates that a recording was stopped.<br>
	 * <br>
	 * <strong>Note:</strong> Currently not used.
	 */
	private class RecordingStopsListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
		}
	}

	/**
	 * Listener for NewVideoMessages.<br>
	 * Activates the ringtone and vibrator to get the doctors attention for a
	 * new video that was just received so he can download, view and rate it.
	 * Saves the video information in the database and sends a broadcast so the
	 * listening activity can update the view.
	 */
	private class NewVideoListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
			alert(RRRRING);

			NewVideoMessage msg = (NewVideoMessage) message;

			Video video = mDataSource.insertVideo(msg.getVideoId(), new Date(), Inbox.NOT_LOADED, msg.getVideoSize());

			if (video != null)
			{
				Intent intent = new Intent(Broadcast.NEW_VIDEO);
				intent.putExtra("de.fhkl.thatsstroke.NewVideo", video);
				sendBroadcast(intent);
			}

			Log.i("Service", "NewVideo Message");
		}
	}

	/**
	 * Listener for VideoMessage.<br>
	 * Cancels all timers to avoid timeouts due to long running file i/o
	 * operations. Writes the received byte data to the SD card. Sends a
	 * broadcast so a listening activity can update the view.<br>
	 * <br>
	 * <strong>Note:</strong>Is called repeatedly: keep it simple and clean!
	 */
	private class VideoListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
			mPingTimer.cancel();
			mTimeoutTimer.cancel();

			VideoMessage msg = (VideoMessage) message;

			UUID id = msg.getVideoId();

			writeVideoFile(id, msg.getVideoData());

			message = null;
			msg = null;

			Video video = mDataSource.containsVideoId(id);

			if (video != null)
			{
				Intent intent = new Intent(Broadcast.VIDEO_TRANSMISSION_PROGRESS_ID);
				intent.putExtra("de.fhkl.thatsstroke.VideoId", UUID.fromString(video.getVideoId()));
				intent.putExtra("de.fhkl.thatsstroke.VideoProgress", video.getProgress());
				sendBroadcast(intent);
			}

			mPingTimer.start(PING_INTERVAL);
		}
	}

	/**
	 * Listener for VideoAssignedMessages.<br>
	 * The video was assigned to an other doctor. It gets removed from the
	 * database and a broadcast is sent, so a listening activity can update the
	 * view.
	 */
	private class VideoAssignedListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
			VideoAssignedMessage msg = (VideoAssignedMessage) message;
			UUID videoId = msg.getVideoId();

			mDataSource.deleteVideo(videoId);

			Intent intent = new Intent(Broadcast.VIDEO_ASSIGNED_ID);
			intent.putExtra("de.fhkl.thatsstroke.VideoAssignedId", videoId);
			sendBroadcast(intent);
		}
	}

	/**
	 * Listener for TransmissionCompletedMessages.<br>
	 * Closes the stream that wrote this video to the SD card. Sends a broadcast
	 * so a listening activity can update the view. The video is now ready to be
	 * viewed.
	 */
	private class TransmissionCompletedListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
			TransmissionCompletedMessage msg = (TransmissionCompletedMessage) message;

			UUID id = msg.getVideoId();

			finalizeVideoFile(id);

			Intent intent = new Intent(Broadcast.TRANSMISSION_COMPLETED_ID);
			intent.putExtra("de.fhkl.thatsstroke.TransmissionCompletedId", id);
			sendBroadcast(intent);

			Log.i("Service", "TransmissionCompleted Message");
		}
	}

	/**
	 * Listener for AcceptRatingMessage.<br>
	 * End of the video rating process, it was transfered, viewed rated, maybe
	 * rerated and the rating was just accepted. Final cleanup of this video in
	 * the database and on the SD card. Sends a broadcast so a listening
	 * activity can update the view.
	 */
	private class AcceptRatingListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
			AcceptRatingMessage msg = (AcceptRatingMessage) message;
			UUID id = msg.getVideoId();

			mDataSource.deleteVideo(id);
			deleteVideoFile(id);

			Intent intent = new Intent(Broadcast.ACCEPT_RATING_ID);
			intent.putExtra("de.fhkl.thatsstroke.AcceptRatingId", id);
			sendBroadcast(intent);

			Log.i("Service", "AcceptRating Message");
		}
	}

	/**
	 * Ping timer sleeps for a specified time and then sends a ping message.
	 * Repeats until canceled.
	 */
	private class PingTimer extends BroadcastReceiver
	{
		/**
		 * The AlarmManager allows us to schedule an alarm that will wake the
		 * device.
		 */
		private AlarmManager mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

		/**
		 * Indicates that the timer is started. Needed to avoid strange behavior
		 * (occurred only once during testing) on reacting to an already sent
		 * ping during the logout process.
		 */
		private boolean mIsStarted;

		@Override
		public void onReceive(Context context, Intent intent)
		{
			String action = intent.getAction();

			if (action.equals(Broadcast.PING_TIMER) && mIsStarted)
			{
				try
				{
					Log.i("Service", "Ping");

					// send ping message
					mOut.send(new PingMessage());
				}
				catch (Exception e)
				{
					Log.e("Service", "Exception " + e.getMessage());
				}

				// start timeout timer
				mTimeoutTimer.start(TIMEOUT_INTERVAL);
			}
		}

		/**
		 * Starts the ping timer that sleeps for the specified time and then
		 * sends a ping message. Repeats until canceled.
		 * 
		 * @param timeInterval
		 */
		public void start(long timeInterval)
		{
			mIsStarted = true;

			Intent intent = new Intent(Broadcast.PING_TIMER);
			PendingIntent pendingIntent = PendingIntent.getBroadcast(StrokeService.this, 0, intent, 0);

			mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeInterval, timeInterval,
					pendingIntent);
		}

		/**
		 * Stops a previously started ping timer.
		 */
		public void cancel()
		{
			mIsStarted = false;

			Intent intent = new Intent(Broadcast.PING_TIMER);
			PendingIntent pendingIntent = PendingIntent.getBroadcast(StrokeService.this, 0, intent, 0);

			mAlarmManager.cancel(pendingIntent);
		}
	}

	/**
	 * Timeout timer that sleeps for a specified time and then tries to
	 * reconnect. Repeats until reconnect was successful or until canceled.
	 */
	private class TimeoutTimer extends BroadcastReceiver
	{
		/** Start back off strategy with a sleep time of 2 seconds */
		public static final int BACKOFF_MIN = 2000;

		/** Stop increasing sleep time at 30 seconds */
		public static final int BACKOFF_MAX = 30000;

		/**
		 * The AlarmManager allows us to schedule an alarm that will wake the
		 * device.
		 */
		private AlarmManager mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

		/** Current sleep time */
		private int mBackoffTimeout = BACKOFF_MIN;

		/**
		 * Indicates that the timer is started. Needed to avoid strange behavior
		 * (occurred only once during testing) on reacting to an already sent
		 * ping during the logout process.
		 */
		private boolean mIsStarted;

		@Override
		public void onReceive(Context context, Intent intent)
		{
			String action = intent.getAction();

			if (action.equals(Broadcast.TIMEOUT_TIMER) && mIsStarted)
			{

				setNotification(SystemStatus.OFFLINE);

				Log.w("Service", "Ping timed out, reconnecting");

				if (!reconnect())
				{
					mTimeoutTimer.start(mBackoffTimeout);

					mBackoffTimeout = Math.min(mBackoffTimeout * 2, BACKOFF_MAX);
				}
			}
		}

		/**
		 * Starts the timeout timer that sleeps for a specified time and then
		 * tries to reconnect. Repeats until reconnect was successful or until
		 * canceled.<br>
		 * Doubles sleep time after every failed reconnect.
		 */
		public void startBackOff()
		{
			start(mBackoffTimeout);
			mBackoffTimeout = Math.min(mBackoffTimeout * 2, BACKOFF_MAX);
		}

		/**
		 * Starts the timeout timer that sleeps for the specified time and then
		 * tries to reconnect. Repeats until reconnect was successful or until
		 * canceled.
		 * 
		 * @param timeInterval
		 */
		public void start(long timeInterval)
		{
			mIsStarted = true;

			Intent intent = new Intent(Broadcast.TIMEOUT_TIMER);
			PendingIntent pendingIntent = PendingIntent.getBroadcast(StrokeService.this, 0, intent, 0);

			mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeInterval, pendingIntent);
		}

		/**
		 * Stops a previously started timeout timer.
		 */
		public void cancel()
		{
			mIsStarted = false;

			Intent intent = new Intent(Broadcast.TIMEOUT_TIMER);
			PendingIntent pendingIntent = PendingIntent.getBroadcast(StrokeService.this, 0, intent, 0);

			mAlarmManager.cancel(pendingIntent);

			mBackoffTimeout = BACKOFF_MIN;
		}
	}

	/**
	 * Allows activities to get a reference of the service.
	 */
	public class StrokeBinder extends Binder
	{
		private WeakReference<StrokeService> mService;

		public StrokeBinder(StrokeService service)
		{
			mService = new WeakReference<StrokeService>(service);
		}

		public StrokeService getService()
		{
			return mService.get();
		}
	}
}